home *** CD-ROM | disk | FTP | other *** search
/ Skunkware 98 / Skunkware 98.iso / src / net / bind-contrib.tar.gz / bind-contrib.tar / contrib / misc / makezones < prev    next >
Encoding:
Text File  |  1996-10-25  |  51.9 KB  |  1,673 lines

  1. #!/bin/perl
  2.  
  3. $version_number = "0.10 [02-Nov-94]";
  4.  
  5. # This script was developed using Perl 4.036. It has also been tested
  6. # with Perl 5.000 (one minor change was needed).
  7.  
  8.  
  9. ########################################################################
  10. #                           MAKEZONES                                  #
  11. ########################################################################
  12.  
  13. # Copyright (c), University of Cambridge, 1993, 1994.
  14. #
  15. # The University retains the copyright and all other legal rights
  16. # to this software and makes it available non-exclusively. All users
  17. # must ensure that the software in all its derivations carries a
  18. # copyright notice as above. No warranty is expressed or implied.
  19.  
  20. # This file is available for anonymous ftp from
  21. #
  22. # ftp.cus.cam.ac.uk:/pub/software/programs/DNS/makezones
  23. #
  24. # Enquires to Philip Hazel <ph10@cus.cam.ac.uk>.
  25.  
  26.  
  27.  
  28. ########################################################################
  29. # CONFIGURATION VARIABLES
  30. #
  31. # These are put at the top for ease of changing. See below for the full
  32. # specification of the script.
  33.  
  34.  
  35. # Makezones checks the characters used in the components of names. Different
  36. # sites may have different local standards in this respect. The variable
  37. # $name_pattern is used to contain a regular expression pattern that
  38. # matches valid components of domain names. Change it to suit your
  39. # requirements. Note that:
  40. #
  41. #  (a) The variable contains only the pattern characters, NOT the delimiting
  42. #      slashes.
  43. #  (b) This pattern is for one component only, so should not contain things
  44. #      that match full stops (periods).
  45. #  (c) The start and end of string metacharacters (^ and $) should not be
  46. #      included; makezones uses this variable to build up a larger pattern
  47. #      to match complete domain names, and it puts in ^ and $ itself.
  48. #  (d) Because it is being constructed as a Perl string, any backslash
  49. #      characters in the pattern must be doubled.
  50.  
  51. # This pattern specifies that names must start with a letter, contain only
  52. # letters, digits, and hyphens, and not end with a hyphen.
  53.  
  54. $name_pattern = '[a-zA-Z]([a-zA-Z\\-\\d]*[a-zA-Z\\d]+)?';
  55.  
  56. # Possible variations:
  57. #
  58. # $name_pattern = '[a-zA-Z\\d]([a-zA-Z\\-\\d]*[a-zA-Z\\d]+)?';  # digit at start
  59. # $name_pattern = '[a-z]([a-z\\-\\d]*[a-z\\d]+)?';              # all lower case
  60. #
  61. # Note that, in addition to this, "*" is permitted as the first component of
  62. # names on MX records, to allow MX wildcarding. Names for PTR records must
  63. # always consist of four numeric components; $name_pattern is not used. Also,
  64. # names on NS records may consist of numeric components - this is necessary
  65. # in order to specify devolved reverse subzones.
  66.  
  67.  
  68. # In a large zone it is very easy to accidentally reuse a name by mistake,
  69. # or as the result of a typo. Makezones checks for duplicate names on A
  70. # and PTR records unless the following variable is set to zero. When checking
  71. # is enabled, it is possible to specify in the source file that certain
  72. # duplicates are to be permitted. See the description of the DUP pseudo-
  73. # RR below. Implied duplicates (i.e. records with no name, that take the 
  74. # name of their predecessor) do not cause errors.
  75.  
  76. $duplicate_name_check = 1;
  77.  
  78.  
  79. # To disable the checking of new zone file lengths against the previous
  80. # versions, set $opt_short = 1 here. This forces the -short option for
  81. # all runs. If a previous version does not exist when a check is required,
  82. # a warning is output, but makezones does not fail.
  83.  
  84. $opt_short = 0;
  85.  
  86.  
  87. # If you want fields in WKS records to be checked against the contents
  88. # of a file for validity, then set $services to the name of the file,
  89. # and $grep to your favourite grep command. The values below will be
  90. # typical. The program searches for the service name followed by a space
  91. # or a tab at the start of a line. If you don't want this check, set
  92. # $services to the null string.
  93.  
  94. $services = "/etc/services";
  95. $grep = "/usr/bin/egrep";
  96.  
  97.  
  98. # If you want makezones to output some commentary as it goes along,
  99. # to let you know it is making some progress, then set the $chatty
  100. # variable to 1.
  101.  
  102. $chatty = 1;
  103.  
  104.  
  105.  
  106. ########################################################################
  107. # UNIX DEPENDENCIES
  108. #
  109. # The Unix "date" command is used to obtain the current date and time
  110. # in a particular format.
  111. #
  112. # Perl's "stat" function is used to obtain the lengths of files; this may
  113. # differ for other operating systems.
  114. #
  115. # Anything else I've forgotten?
  116.  
  117.  
  118.  
  119. ########################################################################
  120. #
  121. # Makezones is a perl script for processing a source file for a DNS zone
  122. # and producing the relevant operational DNS zone files. It does a lot of
  123. # checking to ensure that the data is not bad, and it also ensures that
  124. # the forward and reverse zone information is in step.
  125. #
  126. # Makezones handles the updating of the serial number automatically. It
  127. # does this by updating the SOURCE FILE before generating the zone files.
  128. #                >>>>>      NB NB NB NB      <<<<<
  129. # The source file therefore has to be writeable. Makezones insists that
  130. # the format of the serial number be <year><month><day><version> and that
  131. # the year be four digits long, so that this code will continue to work
  132. # after then end of 1999.
  133. #
  134. # Makezones handles Class B and Class C networks, because those are the
  135. # ones that are around here in Cambridge, UK. It would not be hard to
  136. # extend it to handle a Class A if that were required.
  137. #
  138. # Because the file should normally be correct, makezones makes no attempt
  139. # attempt to continue if it finds a serious error. It just reports it and
  140. # stops. However, syntax errors in the general records don't prevent it
  141. # going on to check further records, so you can get more than one error
  142. # message in a run. However, if it finds too many errors it says so, and
  143. # gives up. "Too many" is currently more than ten.
  144. #
  145. # The input file looks like a normal DNS zone file, with the addition of
  146. # the following rules, which impose additional restrictions. Some of these
  147. # rules are to make it easy for makezones; some of them impose conventions
  148. # that we use in Cambridge which might not be liked elsewhere. The code is
  149. # well commented, and should be easy to modify.
  150. #
  151. #   . The class field ("IN") and the type fields ("A", "CNAME", etc.) must
  152. #     be specified in upper case, as must "TCP" and "UDP" in WKS records.
  153. #
  154. #   . With the exception of the SOA & WKS records, all records must be
  155. #     complete on one line of input. That is, continuation is not supported
  156. #     in general.
  157. #
  158. #   . The SOA record must be right at the start of the file (except for blank
  159. #     and comment lines), and must be set up so that each numeric parameter is
  160. #     on a separate line. For example:
  161. #
  162. #     @    IN    SOA    cus.cam.ac.uk. hostmaster.ucs.cam.ac.uk. (
  163. #                             1993080601      ; Serial
  164. #                             10800           ; Refresh 3 hours
  165. #                             3600            ; Retry 1 hour
  166. #                             604800          ; Expire after a week
  167. #                             86400 )         ; Minimum ttl
  168. #
  169. #     Makezones insists that the serial number be in this date-derived form.
  170. #     Note that the serial number begins with the full year number, not just
  171. #     the last two digits. The SOA record is expected to have the "IN" class
  172. #     field; subsequent records may omit it.
  173. #
  174. #   . The NS records for the zone must appear at the top of the file, just
  175. #     after the SOA record. These will be copied into the forward and the
  176. #     reverse zone files. That is, the default assumption is that the name-
  177. #     servers are the same for the forward and reverse zones. These NS records
  178. #     must NOT have anything in the name field. The copying stops on reaching
  179. #     the first record with a name field or the first non-NS record.
  180. #
  181. #   . Makezones can also cope with the case where there are different NS
  182. #     records for the different zones. If an NS record at the top of the
  183. #     file contains text after the nameserver name, this is taken as a list
  184. #     of zones to which this NS record applies. For example,
  185. #
  186. #            IN    NS    abcd.some.domain.   some.domain.  144.44.0.0
  187. #
  188. #     The reverse zones are identified by their IP network numbers. If there
  189. #     are a lot of them, multiple instances of this special kind of qualified
  190. #     NS record can be used.
  191. #
  192. #   . NS records must always refer to fully qualified names. Makezones checks
  193. #     for the final dot, because it is so easy to overlook this.
  194. #
  195. #   . Comment lines are not normally copied into the working zone files. They
  196. #     can, however, be forced into them by the following syntax:
  197. #
  198. #     ;F   copy this comment line (without the F) into the forward file
  199. #     ;R   copy this comment line (without the R) into the reverse file(s)
  200. #
  201. #   . Comments that are attached to resource records are not copied over
  202. #     into the zone files in most cases.
  203. #
  204. #   . All records except PTR records are normally copied to the forward file.
  205. #     However, A records can be marked as "reverse only" by preceding them
  206. #     with ">R " at the start. In this case, no A record is written to the
  207. #     forward file, but a PTR record is constructed for the appropriate
  208. #     reverse zone file. There should be exactly one space after the ">R";
  209. #     three characters are removed from the start of the record. If ">R" is
  210. #     followed by a tab, the tab is not removed (i.e. it acts as more than
  211. #     one space).
  212. #
  213. #   . PTR records and A records are the only ones used when generating the
  214. #     reverse zone files. "A" records can be marked "forwards only" by preced-
  215. #     ing them with ">F " at the start. This suppresses generation of a PTR
  216. #     record for the reverse zone. It does not, however, suppress the check
  217. #     that the address is in one of the networks being handled (see next item
  218. #     but one for external networks).
  219. #
  220. #   . When several IP addresses are associated with the same domain name,
  221. #     multiple A records are required. Normally, the second and subsequent
  222. #     ones should follow the first record, without a name of their own, thus
  223. #     causing the previous name to be copied. If the same name is in fact
  224. #     present on more than one A (or PTR) record, makezones' duplicate
  225. #     check will pick it up and cause an error, unless (a) duplicate checking
  226. #     has been entirely suppressed or (b) the name is listed in a DUP record.
  227. #
  228. #   . DUP records are something invented just for makezones; they are not
  229. #     part of DNS zone files and do not cause anything to be written to the
  230. #     output files. The format of a DUP record is:
  231. #
  232. #     domain-name      DUP
  233. #
  234. #     A DUP record tells makezones that its name is expected to appear on
  235. #     more than one A or PTR record, and this is not an error. The DUP 
  236. #     record can appear in the file anywhere before the second record with 
  237. #     the given name. (Putting all the DUP records together near the top is
  238. #     one way of keeping all this information in one place.)
  239. #
  240. #   . If more than one A record has the same IP address, there are four
  241. #     possibilities:
  242. #
  243. #       (1) This is an error in the input.
  244. #
  245. #       (2) One name is considered "canonical" and reverse lookups on the
  246. #           address should yield this name.
  247. #
  248. #       (3) Reverse lookups on the address should yield all of the names.
  249. #
  250. #       (4) Reverse lookups on the address should yield more than one (but not
  251. #           all) of the names.
  252. #
  253. #     By default, makezones assumes case (1), because typos are really easy to
  254. #     make when handling IP addresses. It therefore produces an error message
  255. #     in cases such as this:
  256. #
  257. #       some.name       A  199.99.99.99
  258. #       other.name      A  199.99.99.99
  259. #
  260. #     If case (2) applies, then all but one of the records must have the ">F "
  261. #     flag, to ensure that only one PTR record is generated (for the canonical
  262. #     name). Again, there must be exactly one space or a tab after ">F". For
  263. #     example:
  264. #
  265. #       canon.name      A  199.99.99.99
  266. #       >F other.name   A  199.99.99.99
  267. #
  268. #     If case (3) applies, then all the records, except (optionally) the
  269. #     first, must have the ">M " flag, to tell makezones that multiple PTR
  270. #     records are required. It is probably helpful to put the flag on the 
  271. #     first record as well, as a reminder that other records exist, especially
  272. #     if they are separated in the input file. For example:
  273. #
  274. #       >M some.name    A  199.99.99.99
  275. #       >M other.name   A  199.99.99.99
  276. #
  277. #     Case (4) is just a mixture of cases (2) and (3), with some records having
  278. #     the ">M " flag and some the ">F " flag.
  279. #
  280. #   . We want to be able to check that all IP addresses are in one of the
  281. #     networks that we are processing for. However, occasionally a record must
  282. #     specify an external network (glue records are the prime example). Such
  283. #     records must be flagged by ">E " at their start to override the error
  284. #     that would otherwise occur. (They naturally won't get into any reverse
  285. #     zones.) The special local address 127.0.0.1 is recognized and treated as
  286. #     though ">E " is always present. The ">E " flag can be used on WKS
  287. #     records as well as on A records.
  288. #
  289. #   . The name given for PTR records must be a complete, reversed IP address
  290. #     that corresponds to one of the reverse zones. The network portion of
  291. #     the "name" is removed when generating the PTR record for the reverse
  292. #     zone.
  293. #
  294. #   . The ">M " flag may be used with PTR records if multiple entries
  295. #     for the same IP address are required (see the comments about cases of
  296. #     more than one name for the same IP number above). If this is done,
  297. #     and the name (i.e. the reversed IP address) is explicitly quoted on
  298. #     the second or subsequent records, it must also be listed in a DUP
  299. #     record, unless duplicate checking is disabled.
  300. #
  301. #   . Very few PTR records should ever be necessary, but PTR records have to
  302. #     be used instead of A records flagged with ">R " ("reverse only") when
  303. #     the name pointed to is not in the domain of the forward zone, because
  304. #     of the following rule:
  305. #
  306. #   . The names on all records must not end with . as we conventionally
  307. #     specify them as partial domains for the forward zone. This means that,
  308. #     if you want a record with the name of the zone as its domain, you must
  309. #     use the "@" notation, which is supported.
  310. #
  311. #   . Makezones assumes that names consist of letters and digits, and start
  312. #     with a letter. You can, however, override this by enclosing a name
  313. #     in quotes. For example:
  314. #
  315. #     "3cpu"   A     134.232.45.69
  316. #
  317. #     I didn't want to allow these through normally, as in my zone they are
  318. #     more likely to be typos. You can change the rules for what characters
  319. #     are allowed in names (without quoting) by editing the variable
  320. #     $name_pattern (see under CONFIGURATION VARIABLES at the head of this
  321. #     file).
  322. #
  323. #   . There are occasions when you want to ensure that a name is *not*
  324. #     present in your zone, for example, if you are reserving it for some
  325. #     specific future use and don't want it used for something else by
  326. #     mistake. The RESERVE record, which is a facility local to makezones,
  327. #     can be used for this. If a record such as
  328. #
  329. #     do-not-use-me   RESERVE
  330. #
  331. #     is encountered by makezones, it performs its normal duplicate checking
  332. #     on the name as if it were an A record, but generates no output from 
  333. #     this record.
  334. #
  335. #   . CNAME records must point to fully qualified names. Makezones checks
  336. #     that if a name appears on a CNAME, it does not appear on any other
  337. #     record.
  338. #
  339. #   . MX records must point to fully qualified names.
  340. #
  341. #
  342. # Makezones is run by a command of the following form:
  343. #
  344. #   makezones [options] <source> <forward-zone> <forward-zone-file> \
  345. #     [<reverse-zone-file>]*
  346. #
  347. # For example:
  348. #
  349. #   makezones  DBsource  cam.ac.uk  db.cam  db.131.111  db.192.153.213
  350. #
  351. # The source file is specified as the first argument. The second and third
  352. # arguments specify the name of the zone and the file into which the records
  353. # for that zone are to be written. The name is required so that fully
  354. # qualified names can be generated in the reverse zone files. The remaining
  355. # arguments specify the networks for which reverse zone files are to be
  356. # written, and the corresponding files. There need not be any if there are
  357. # no PTR or non-forwards-only A records in the source file. Each of these
  358. # final arguments is the name of a zone file. The first part of the name can
  359. # be anything you like - the only requirement is that the name must end with
  360. # a valid Class B or Class C network number.
  361. #
  362. # [This combining of network number and zone file name is done for convenience.
  363. # To change makezones so that the numbers and file names are given as separate
  364. # arguments would not be difficult; the changes would affect only the sub-
  365. # routine that unpicks the arguments.]
  366. #
  367. # It is intended that makezones will normally be run as part of a "make"
  368. # sequence which will also install the files and reload the nameserver(s)
  369. # after makezones has run successfully. Thus, the command to run it will
  370. # normally be stored in a file and not typed each time.
  371. #
  372. # The output files are actually written to temporary files whose names are the
  373. # same as the final ones with ".new" appended. If the processing succeeds,
  374. # these files are renamed; if it fails, they are deleted.
  375. #
  376. # Normally no options are required. There is currently only one option:
  377. #
  378. #   -short   Used when a new zone file is more than 5% shorter than the
  379. #            previous version. If not given, the processing will fail if
  380. #            a new file is that much shorter. This guards against the case
  381. #            of accidental loss of large portions of the source file. Setting
  382. #            -short disables the length checking for all zones. You do not
  383. #            need to set this option if the previous versions of the files 
  384. #            do not exist, as in that case a warning is given, but makezones
  385. #            continues. The script can be configured to default to -short; see
  386. #            "configuration options" above.
  387. #
  388. # The input file must be writable. The first thing the script does is to update
  389. # the serial number in the original file. This forms a permanent record and
  390. # ensures that all the created zones have the same number. The form of the
  391. # serial number must be <year><month><day><sequence>, as in the example SOA
  392. # record shown above. The code will continue to work after December 31, 1999.
  393. # If more than 99 updates are done in one day, the failure is soft in that a
  394. # valid serial number is still generated, though it no longer contains that
  395. # day's date.
  396. #
  397. #
  398. # Written by Philip Hazel <ph10@cus.cam.ac.uk>
  399. #   University Computing Service
  400. #   Computer Laboratory
  401. #   New Museums Site
  402. #   Cambridge CB2 3QG
  403. #   United Kingdom
  404. #   +44 1223 334714
  405. #
  406. # Started: August 1993
  407. # Running: September 1993
  408. #
  409. # Update history:
  410. #   0.03   07-Sep-93  I'd forgotten to allow TTLs on SOA records.
  411. #   0.04   08-Sep-93  Allow comments before the SOA record.
  412. #                     In several places, " " appeared in calls to split(),
  413. #                       where "\s" should have appeared.
  414. #                     Allow non-standard names in quotes. This lets in
  415. #                       names like "3cpu" and "*.something".
  416. #                     Treat tabs after >F etc as multiple spaces.
  417. #                     Allow the name "@"; replace by zone name + dot.
  418. #                     Allow omission of class field except on the SOA record.
  419. #                     Check WKS address is in known network unless >E given.
  420. #                     Fail broadcast addresses.
  421. #   0.05   09-Sep-93  Use $name_pattern to check names.
  422. #                     Permit "*" as first name component on MX records.
  423. #   0.06   10-Sep-93  Failed if trailing spaces followed 127.0.0.1
  424. #   0.06a  22-Sep-93  Updated the specification comments.
  425. #   0.07   05-Nov-93  Added support for RP records.
  426. #                     Added conditional facility for zone NS records.
  427. #   0.08   09-Sep-94  Added the ">M " flag to permit multiple PTR records.
  428. #                     Incorporated duplicate name checking and the DUP
  429. #                       pseudo-record, and merged the CNAME check into
  430. #                       this code as well. Uses an associative array, which
  431. #                       will be large for large zones, but no larger than
  432. #                       the existing one already used for addresses.
  433. #                     Don't fail if previous version of a zone file does
  434. #                       not exist (for length checking). Just say so.
  435. #                     Support comments on the ends of all records.
  436. #   0.09   01-Nov-94  Added /o to the pattern matches involving $name_pattern.
  437. #                     Added the RESERVE record.
  438.  
  439.  
  440.  
  441. ##################################################
  442. #            Print error message and die         #
  443. ##################################################
  444.  
  445. # Ensure any temporary files are removed first. If reading the main file,
  446. # $nline will be set non-zero and the current line will be in $_.
  447.  
  448. sub give_up {
  449. do remove_temps();
  450. print "\n** Makezones: $_[0]\n";
  451. if ($nline > 0)
  452.   {
  453.   print "   At line $nline of $source_file:\n";
  454.   print "   $_";
  455.   }
  456. die "** Processing abandoned.\n\n";
  457. }
  458.  
  459.  
  460.  
  461. ##################################################
  462. #       Print error message and continue         #
  463. ##################################################
  464.  
  465. # After too many errors, give up.
  466.  
  467. sub error {
  468. print "\n** Makezones: $_[0]\n";
  469. if ($nline > 0)
  470.   {
  471.   print "   At line $nline of $source_file:\n";
  472.   print "   $_";
  473.   }
  474. if (++$errors > 10)
  475.   {
  476.   do remove_temps();
  477.   die "\n** Makezones: too many errors - processing abandoned.\n\n";
  478.   }
  479. }
  480.  
  481.  
  482.  
  483. ##################################################
  484. #       Print line to all reverse zone files     #
  485. ##################################################
  486.  
  487. sub print_reverse {
  488. local($i);
  489. for ($i = 0; $i < $rzone_count; $i++)
  490.   {
  491.   local($handle) = "REVERSE$i";
  492.   print $handle $_[0];
  493.   }
  494. }
  495.  
  496.  
  497.  
  498. ##################################################
  499. #            Unpick the argument list            #
  500. ##################################################
  501.  
  502. # Exit from the whole program on failure.
  503.  
  504. sub unpick_args {
  505. $rzone_count = 0;
  506.  
  507. # Handle options
  508.  
  509. while ($#ARGV >= 0 && substr($ARGV[0], 0, 1) eq '-')
  510.   {
  511.   if ($ARGV[0] eq "-short")  { $opt_short = 1; }
  512.   else { do give_up("unknown option \"$ARGV[0]\""); }
  513.   shift ARGV;
  514.   }
  515.  
  516. # Now we should be left with at least four arguments
  517.  
  518. do give_up("at least three arguments are needed") if $#ARGV < 2;
  519.  
  520. # The first argument is the source file
  521.  
  522. $source_file = $ARGV[0]; shift ARGV;
  523.  
  524. # The second argument is the zone name; remove the trailing dot
  525. # if present.
  526.  
  527. $zone_name = $ARGV[0]; shift ARGV;
  528. chop($zone_name) if (substr($zone_name, -1, 1) eq ".");
  529.  
  530. # The third argument is the forwards zone file
  531.  
  532. $forward_file = $ARGV[0]; shift ARGV;
  533.  
  534. # We now have zero or more reverse zone files
  535.  
  536. while ($#ARGV >= 0)
  537.   {
  538.   local($rzone) = $ARGV[0]; shift ARGV;
  539.   $rzone_file[$rzone_count] = $rzone;
  540.  
  541.   # Check explicitly for a class B or a class C number. I couldn't
  542.   # find a cunning way of writing a single regular expression that
  543.   # handled this. Anyway, we need to differentiate in order to check
  544.   # the values.
  545.  
  546.   local($a,$b,$c) = $rzone =~ /^.*\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
  547.  
  548.   if ("$a" eq "")
  549.     {
  550.     ($a,$b) = $rzone =~ /^.*\.(\d{1,3})\.(\d{1,3})$/;
  551.     do give_up("\"$rzone\" does not end with a class B or C ".
  552.       "network number") if $a eq "";
  553.     do give_up("bad class B network $a.$b") if ($a < 128 || $a > 191);
  554.     $rzone_number[$rzone_count++] = ($a << 24) | ($b << 16);
  555.     }
  556.   else
  557.     {
  558.     do give_up("bad class C network $a.$b.$c")
  559.       if ($a < 192 || $a > 223);
  560.     $rzone_number[$rzone_count++] = ($a << 24) | ($b << 16) | ($c << 8);
  561.     }
  562.   }
  563. }
  564.  
  565.  
  566.  
  567. ##################################################
  568. #         Verify what we are going to do         #
  569. ##################################################
  570.  
  571. sub verify {
  572. print "\nMakezones $version_number\n";
  573. print "Generating DNS zone files for $zone_name from $source_file.\n";
  574. print "  Forward zone file:  $forward_file\n";
  575. printf "  Reverse zone file%s ", ($rzone_count == 1)? ": " : "s:";
  576.  
  577. if ($rzone_count > 0)
  578.   {
  579.   for ($i = 0; $i < $rzone_count; $i++)
  580.     {
  581.     print " "x22 if $i != 0;
  582.     print "$rzone_file[$i]\n";
  583.     }
  584.   }
  585. else { print "<none>\n"; }
  586. }
  587.  
  588.  
  589.  
  590. ##################################################
  591. #           Update the serial number             #
  592. ##################################################
  593.  
  594. # This function also checks out the format of the SOA
  595. # record at the top of the file. We require it to be split
  596. # so that every field is on a different line.
  597.  
  598. sub update_serial {
  599. local($i);
  600. print "\nUpdating the serial number in the source file...\n" if $chatty;
  601. open(SOURCE, "+<$source_file") ||
  602.   do give_up("unable to open $source_file for read/write (to update serial)");
  603.  
  604. # Check out the first line as the start of the SOA data. Skip any
  605. # prior comments, counting them so that we know how many lines to
  606. # copy when copying the SOA data.
  607.  
  608. for (;;)
  609.   {
  610.   $_ = <SOURCE>;
  611.   last if (!/^\s*$/ && !/^\s*;/);
  612.   $soa_count++;
  613.   }
  614.  
  615. local($host,$hostmaster);
  616. local($at,$rest) = split(/\s+/, $_, 2);
  617. if ($rest =~ /^\d/)
  618.   {
  619.   ($ttl,$host,$hostmaster) =
  620.     $rest =~ /^(\d+)\s+IN\s+SOA\s+(\S+)\s+(\S+)\s*\($/;
  621.   }
  622. else
  623.   {
  624.   ($host,$hostmaster) = $rest =~ /^IN\s+SOA\s+(\S+)\s+(\S+)\s*\($/;
  625.   }
  626.  
  627. do give_up("malformed SOA record")
  628.   if ($at ne "@" || $host eq "" || $hostmaster eq "");
  629.  
  630. # Remember where to write the second line, read it, and fish
  631. # out the serial number.
  632.  
  633. local($pos) = tell SOURCE;
  634. $_ = <SOURCE>;
  635. local($indent,$value) = /^(\s+)(\d{10})(\s*;.*|)$/;
  636. do give_up("malformed serial number line (line 2 of SOA)") if ($value eq "");
  637.  
  638. # Check out the remaining lines of the SOA record
  639.  
  640. for ($i = 3; $i <= 6; $i++)
  641.   {
  642.   $_ = <SOURCE>;
  643.   local($check) = ($i == 6)? /^\s+(\d+)\s*\)(\s*;.*|)$/ : /^\s+(\d+)(\s*;.*|)$/;
  644.   do give_up("line $i of the SOA record is malformed") if ($check eq "");
  645.   }
  646.  
  647. # Calculate the serial number for the first update of
  648. # today, allowing for the impending millenium.
  649.  
  650. local($today_serial) = `date +20%y%m%d01`;
  651. $today_serial -= 100000000 if (substr($today_serial, 2, 2) > 90);
  652.  
  653. # If the existing serial number is already >= today's
  654. # start, increment it by one. Otherwise use today's start.
  655.  
  656. $value = ($value >= $today_serial)? $value+1 : $today_serial;
  657.  
  658. # Re-write the start of the second record with the new serial number.
  659.  
  660. seek(SOURCE, $pos, 0);
  661. print SOURCE "$indent$value";
  662. close SOURCE;
  663. }
  664.  
  665.  
  666.  
  667.  
  668.  
  669. ##################################################
  670. #          Handle comment lines                  #
  671. ##################################################
  672.  
  673. sub handle_comment{
  674. if (/^;F /)
  675.   {
  676.   printf FORWARD "; %s", substr($_, 3);
  677.   }
  678. elsif (/^;R /)
  679.   {
  680.   do print_reverse(join("", "; ", substr($_, 3)));
  681.   }
  682. }
  683.  
  684.  
  685.  
  686.  
  687.  
  688. ##################################################
  689. # Check final field is a fully-qualified name    #
  690. ##################################################
  691.  
  692. sub check_fqn{
  693. do error("$_[1] record must point to a valid, fully qualified name.")
  694.   if ($_[0] !~ /^[a-zA-Z][a-zA-Z\d\-]*(\.[a-zA-Z][a-zA-z\d\-]*)*\.$/)
  695. }
  696.  
  697.  
  698.  
  699.  
  700. ##################################################
  701. #              Handle non-comment records        #
  702. ##################################################
  703.  
  704. # The record is stored in $_ on entry. Do not alter this, since it is
  705. # reflected after an error message. However, is is permitted to read a
  706. # continuation record into it (as is done for WKS handling).
  707.  
  708. # Two associative arrays, %names and %addresses, are used for checking
  709. # on the duplication of names and addresses. The check for CNAME and
  710. # other data is handled by the same mechanism. The values used in the
  711. # %names array are:
  712. #
  713. #   n == undef          the name has not yet been seen
  714. #   n > 0               the name has appeared on one A (or PTR) record
  715. #   $used_dup < n < 0   the name has appeared on a CNAME record
  716. #   $used_dup           the name has appeared on a DUP record
  717. #   $used_reserve       the name has appeared on a RESERVE record
  718. #   $used_other         the name has appeared on any other DNS record
  719. #
  720. # The named values are all large negative numbers.
  721. #
  722. # An appearance on an A or PTR record overrides $user_other, and an appearance
  723. # on a DUP record overrides a value > 0 and $used_other. The first
  724. # entry to $names is set up when handling the SOA record. The values
  725. # used for A, PTR and CNAME records are the line numbers where the first
  726. # instance occurred (for use in error messages); to distinguish CNAME
  727. # records, the line number is negated.
  728.  
  729.  
  730. sub handle_record {
  731. $forwards_only = $reverse_only = $external_net = $multiple = 0;
  732.  
  733. # If the record starts with ">E ", ">F ", ">M " or ">R " set flags for 
  734. # later checks once the type of record is known, and remove these characters. 
  735. # $forwards_only must always be set if $external_net is set. If ">E" etc. are 
  736. # followed by a tab, this must be interpreted as if it were several spaces; 
  737. # the right thing happens if the tab is not removed.
  738.  
  739. if (/^>E\s/)
  740.   {
  741.   $forwards_only = $external_net = 1;
  742.   $rest = substr($_, (substr($_,2,1) eq " ")? 3:2);
  743.   }
  744. elsif (/^>F\s/)
  745.   {
  746.   $forwards_only = 1;
  747.   $rest = substr($_, (substr($_,2,1) eq " ")? 3:2);
  748.   }
  749. elsif (/^>M\s/)
  750.   {
  751.   $multiple = 1;
  752.   $rest = substr($_, (substr($_,2,1) eq " ")? 3:2);
  753.   }
  754. elsif (/^>R\s/)
  755.   {
  756.   $reverse_only = 1;
  757.   $rest = substr($_, (substr($_,2,1) eq " ")? 3:2);
  758.   }
  759. else
  760.   { $rest = $_; }
  761.  
  762. # Split the line into the first field (name) and the rest
  763. # of the line. Name is null if the line starts with a space.
  764. # In this case, set it to the value from the previous record,
  765. # but set the printing name to blanks so it isn't output.
  766. # We still use split() in this case, because it gets rid
  767. # of the leading spaces on the remainder of the line.
  768.  
  769. ($name,$rest) = split(/\s+/, $rest, 2);
  770. if ($name eq "")
  771.   {
  772.   $name = $lastname;
  773.   $printname = "  ";
  774.   }
  775. else
  776.   {
  777.   $printname = $name;
  778.   $lastname = $name;
  779.   }
  780.  
  781. # If $name is null, it means we have hit a record without a name
  782. # field at the top of the file. In a zone file this would mean the
  783. # name of the zone, but we don't allow this laxness.
  784.  
  785. if ($name eq "")
  786.   {
  787.   do error("missing name on the first record after initial SOA + NS records.");
  788.   return;
  789.   }
  790.  
  791. # Split off the TTL field, if present. It must consist entirely
  792. # of digits.
  793.  
  794. if ($rest =~ /^\d/)
  795.   {
  796.   ($ttl,$rest) = split(/\s+/, $rest, 2);
  797.   if ($ttl ne "" && $ttl !~ /^\d+$/)
  798.     {
  799.     do error("invalid TTL field (not all digits).");
  800.     return;
  801.     }
  802.   }
  803. else { $ttl = ""; }
  804.  
  805. # The class field may or may not be present. If not, the rule is to
  806. # copy it from the previous record, but we support only the "IN"
  807. # class anyway.
  808.  
  809. ($class,$rest) = split(/\s+/, $rest, 2);
  810. if ($class eq "IN")
  811.   {
  812.   ($type,$rest) = split(/\s+/, $rest, 2);
  813.   }
  814. else
  815.   {
  816.   $type = $class;
  817.   $class = "";
  818.   }
  819.  
  820. # Forward-only, reverse-only, external, and multiple flags may be
  821. # specified only for A records, except that >E may be specified for
  822. # WKS records, and >M for PTR records.
  823.  
  824. if ($multiple)
  825.   {
  826.   do error(">M may be specified only for type A or type PTR records.")
  827.     if ($type ne "A" && $type ne "PTR");
  828.   }
  829. elsif ($external_net)
  830.   {
  831.   do error(">E may be specified only for type A or type WKS records.")
  832.     if ($type ne "A" && $type ne "WKS");
  833.   }
  834. else
  835.   {
  836.   do error(">F and >R may be specified only for type A records.")
  837.     if (($forwards_only || $reverse_only) && $type ne "A");
  838.   }
  839.  
  840. # If the name's components all consists of digits, it it taken as a
  841. # reversed IP address for inclusion in the reverse zone. Otherwise its
  842. # components must match the pattern set in the $name_pattern variable.
  843. # It may not end with a dot, as it is a subdomain name. Repeated names
  844. # get checked twice, but this isn't a great overhead.
  845. #
  846. # To allow for exceptions to the general $name_pattern check, we permit
  847. # names in double quotes. These are not checked at all.
  848. #
  849. # We must also allow the name "@" so that people can set up, for example,
  850. # MX records for their entire zone, and we allow the first component of
  851. # names on MX records to be "*".
  852.  
  853. if ($name eq "@")
  854.   {
  855.   $name = "$zone_name.";
  856.   $printname = $name if (substr($printname, 0, 1) ne " ");
  857.   }
  858. elsif ($name =~ /^\*\./)
  859.   {
  860.   if ($name !~ /^\*\.$name_pattern(\.$name_pattern)*$/o)
  861.     {
  862.     do error("invalid wildcard name field\n".
  863.              "** (or other components do not match name pattern).");
  864.     $name = $lastname = "dummy";     # prevent subsequent errors
  865.     }
  866.   elsif ($type ne "MX")
  867.     {
  868.     do error("wildcard names are permitted only on MX records.");
  869.     $name = $lastname = "dummy";     # prevent subsequent errors
  870.     }
  871.   }
  872. elsif (substr($name, 0, 1) eq "\"" && substr($name, -1) eq "\"")
  873.   {
  874.   $name = substr($name, 1, length($name) - 2);
  875.   $printname = $name if (substr($printname, 0, 1) ne " ");
  876.   }
  877. elsif ($name =~ /^\d{1,3}(\.\d{1,3})*$/)
  878.   {
  879.   # Just check that this is on a PTR or NS or DUP record - full checking 
  880.   # of the name happens later for PTR & NS records.
  881.   if ($type ne "PTR" && $type ne "NS" && $type ne "DUP")
  882.     {
  883.     do error("invalid name field for this type of record.");
  884.     $name = $lastname = "dummy";     # prevent subsequent errors
  885.     }
  886.   }
  887. elsif ($name !~ /^$name_pattern(\.$name_pattern)*$/o)
  888.   {
  889.   do error("invalid name field (components do not match name pattern).");
  890.   $name = $lastname = "dummy";     # prevent subsequent errors
  891.   }
  892.   
  893.  
  894.  
  895. # If the name on this record previously appeared on a RESERVE
  896. # record, it is an error. Let processing continue, however, to
  897. # detect other errors.
  898.  
  899. if ($names{"$name"} == $used_reserve)
  900.   {
  901.   do error("$name appeared on a previous RESERVE record.");
  902.   }    
  903.  
  904.  
  905. # If the name on this record, explicit or implied, previously
  906. # appeared on a CNAME record, it is an error. Set the value back
  907. # to nothing, to prevent multiple complaints.
  908.  
  909. if ($names{"$name"} < 0 && $names{"$name"} > $used_dup)
  910.   {
  911.   $temp = - $names{"$name"}; 
  912.   do error("$name appears on a previous CNAME record (line $temp).");
  913.   $names{"$name"} = ""; 
  914.   }       
  915.   
  916.  
  917.  
  918.  
  919. # Now we perform individual check which depend on the
  920. # record's type field. We support only the following types:
  921. # A, NS, CNAME, PTR, HINFO, MX, TXT, WKS, RP, and the special
  922. # DUP (invented for makezones).
  923.  
  924. # For all except TXT, we must ignore trailing spaces and anything 
  925. # following the first semicolon on the line, since that introduces 
  926. # a comment. This is not quite so simple for TXT, because of the 
  927. # quotes, so we handle TXT separately.
  928.  
  929.  
  930. # Type TXT - arbitrary descriptive text, enclosed in double quotes
  931.  
  932. if ($type eq "TXT")
  933.   {
  934.   if ($rest !~ /^\".*\"\s*(;.*)?$/)
  935.     {
  936.     do error("malformed TXT record - must use double quotes.");
  937.     }
  938.   print FORWARD "$printname  $ttl  $class  TXT  $rest";
  939.   $names{"$name"} = $used_other if $names{"$name"} == "";
  940.   return;
  941.   }
  942.  
  943.  
  944. # Remove comments and trailing spaces for all other types. This also
  945. # removes the trailing newline.
  946.  
  947. $rest =~ s/\s*(;.*)?$//;
  948.  
  949.  
  950.  
  951. # Type RESERVE - a locally invented feature to reserve a name for
  952. # future use. Complain if the name has been previously used; otherwise
  953. # set a value in the names array to reserve it.
  954.  
  955. if ($type eq "RESERVE")
  956.   {
  957.   do error("malformed RESERVE record (text after RESERVE).") if $rest !~ /^$/;
  958.   if ($names{"$name"} ne "")
  959.     {
  960.     do error("reserved name $name previously used."); 
  961.     }  
  962.   else { $names{"$name"} = $used_reserve; }
  963.   return;
  964.   }  
  965.  
  966.  
  967.  
  968. # Type A - host address; the address must be in one of the networks
  969. # being processed, unless it was flagged as an external network.
  970.  
  971. if ($type eq "A")
  972.   {
  973.   local($rzone);
  974.   local($nn) = $names{"$name"}; 
  975.   local($a,$b,$c,$d) =
  976.     $rest =~ /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
  977.     
  978.   if ($duplicate_name_check && $printname !~ /^\s*$/ && $nn > 0)
  979.       {
  980.       do error("unexpected duplicate name.\n".
  981.         "** The first occurrence was in line $nn.");
  982.       }
  983.       
  984.   $names{"$name"} = $nline if $nn == "" || $nn == $used_other;
  985.      
  986.   if ($a eq "")
  987.     {
  988.     do error("IP address is incomplete.");
  989.     return;
  990.     }
  991.  
  992.   if ($a > 255 || $b > 255 || $c > 255 || $d > 255)
  993.     {
  994.     do error("IP address contains component with value greater than 255.");
  995.     return;
  996.     }
  997.  
  998.   do error ("broadcast address not allowed.")
  999.     if (($a >= 192 && $d == 255) || ($a < 192 && $c == 255 && $d == 255));
  1000.  
  1001.   # The loopback address is always treated as external
  1002.  
  1003.   $external_net = $forwards_only = 1 if ($rest =~ /^\s*127\.0\.0\.1\s*$/);
  1004.  
  1005.   # Check known network (& find network) unless external
  1006.  
  1007.   if (!$external_net)
  1008.     {
  1009.     local($net) = ($a << 24) | ($b << 16);
  1010.     $net += ($c << 8) if $a >= 192;
  1011.  
  1012.     for ($rzone = 0; $rzone < $rzone_count; $rzone++)
  1013.       { last if ($net == $rzone_number[$rzone]); }
  1014.  
  1015.     if ($rzone >= $rzone_count)
  1016.       {
  1017.       do error("IP address is not in a known network (use >E for externals).");
  1018.       return;
  1019.       }
  1020.     }
  1021.  
  1022.   # Output the A record to the forward file, unless reverse-only record.
  1023.  
  1024.   print FORWARD "$printname  $ttl  $class  A  $rest\n" if !$reverse_only;
  1025.  
  1026.   # If required, generate a PTR record for the reverse file. Check for
  1027.   # multiples, and complain unless the record is flagged as such.
  1028.  
  1029.   if (!$forwards_only)
  1030.     {
  1031.     $thisaddress = "$a.$b.$c.$d";
  1032.     if ($addresses{"$thisaddress"} != "" && !$multiple)
  1033.       {
  1034.       do error("duplicate IP address $thisaddress specified for a PTR record.\n".
  1035.         "** Use the >M flag if multiple PTR records are required.\n".
  1036.         "** The first occurrence was in line $addresses{$thisaddress}.");
  1037.       }
  1038.     else
  1039.       {
  1040.       local($handle) = "REVERSE$rzone";
  1041.       print $handle "$d";
  1042.       print $handle ".$c" if ($a < 192);
  1043.       print $handle "  $ttl  $class  PTR  $name";
  1044.       print $handle ".$zone_name." if (substr($name, -1, 1) ne ".");
  1045.       print $handle "\n";
  1046.       $addresses{"$thisaddress"} = $nline
  1047.         if $addresses{"$thisaddress"} == "";
  1048.       }
  1049.     }
  1050.  
  1051.   return;
  1052.   }
  1053.  
  1054.  
  1055.  
  1056. # Type CNAME - pointer to canonical name. We require the canonical
  1057. # name to be fully qualified. We also want to check that any name
  1058. # that is on a CNAME record does not also appear on any other records.
  1059. # This is done via the %names associative array. If there was a previous
  1060. # CNAME record, the error message has already been given (and the value
  1061. # set back to null to prevent another one).
  1062.  
  1063. if ($type eq "CNAME")
  1064.   {
  1065.   local ($nn) = $names{"$name"}; 
  1066.   do check_fqn($rest, "CNAME");
  1067.   if ($nn == "")
  1068.     {
  1069.     $names{"$name"} = - $nline;
  1070.     print FORWARD "$name  $ttl $class  CNAME  $rest\n";
  1071.     }
  1072.   else
  1073.     {
  1074.     if ($nn > $used_dup)
  1075.       {  
  1076.       $nn = - $nn if $nn < 0; 
  1077.       do error("$name appears on a previous record (line $nn).");
  1078.       }
  1079.     else
  1080.       {
  1081.       do error("$name appears on a previous record.");
  1082.       }       
  1083.     }
  1084.   return;
  1085.   }
  1086.  
  1087.  
  1088.  
  1089. # Type PTR - pointer to entity elsewhere in the DNS; used only
  1090. # for explicit reverse-lookup entries when the name is not in
  1091. # this forwards zone. The name must be a complete reversed
  1092. # IP address.
  1093.  
  1094. if ($type eq "PTR")
  1095.   {
  1096.   local($net, $rzone);
  1097.   local ($nn) = $names{"$name"}; 
  1098.   local($a,$b,$c,$d) =
  1099.     $name =~ /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
  1100.  
  1101.   if ($duplicate_name_check && $printname !~ /^\s*$/ && $nn > 0)
  1102.       {
  1103.       do error("unexpected duplicate name.\n".
  1104.         "** The first occurrence was in line $nn.");
  1105.       }
  1106.       
  1107.   $names{"$name"} = $nline if $nn == "" || $nn == $used_other;
  1108.  
  1109.   if ($a eq "")
  1110.     {
  1111.     do error("name on PTR record must be complete IP address");
  1112.     return;
  1113.     }
  1114.  
  1115.   if ($a > 255 || $b > 255 || $c > 255 || $d > 255)
  1116.     {
  1117.     do error("IP address contains component with value greater than 255.");
  1118.     return;
  1119.     }
  1120.  
  1121.   do check_fqn($rest, "PTR");
  1122.  
  1123.   $net = ($d << 24) | ($c << 16);
  1124.   $net += ($b << 8) if $d >= 192;
  1125.  
  1126.   for ($rzone = 0; $rzone < $rzone_count; $rzone++)
  1127.     { last if ($net == $rzone_number[$rzone]); }
  1128.  
  1129.   if ($rzone >= $rzone_count)
  1130.     {
  1131.     $net = ($d >= 192)? "$d.$c.$b" : "$d.$c";
  1132.     do error("$net is not a known network.");
  1133.     }
  1134.   else
  1135.     {
  1136.     $thisaddress = "$d.$c.$b.$a";
  1137.     if ($addresses{"$thisaddress"} != "" && !$multiple)
  1138.       {
  1139.       do error("duplicate IP address $thisaddress specified for a PTR record.\n".
  1140.         "** Use the >M flag if multiple PTR records are required.\n".
  1141.         "** The first occurrence was in line $addresses{$thisaddress}.");
  1142.       }
  1143.     else
  1144.       {
  1145.       local($handle) = "REVERSE$rzone";
  1146.       print $handle "$a";
  1147.       print $handle ".$b" if $d < 192;
  1148.       print $handle "  $ttl $class  PTR  $rest\n";
  1149.       $addresses{"$thisaddress"} = $nline
  1150.         if $addresses{"$thisaddress"} == "";
  1151.       }
  1152.     }
  1153.  
  1154.   return;
  1155.   }
  1156.   
  1157.  
  1158.  
  1159.  
  1160. # Type DUP - a pseudo record invented for use by makezones,
  1161. # specifying that the name is permitted to be duplicated on
  1162. # A and PTR records. If this name appeared on a previous CNAME,
  1163. # an error will already have been given. Further errors might
  1164. # occur whether or not we override, so take the easy line.
  1165.  
  1166. if ($type eq "DUP")
  1167.   {
  1168.   do error("malformed DUP record (text after DUP).") if $rest !~ /^$/;
  1169.   $names{"$name"} = $used_dup;
  1170.   return; 
  1171.   }   
  1172.  
  1173.  
  1174.  
  1175. # The remaining record types are classified as "other" for the
  1176. # purpose of remembering which names have been used. This is
  1177. # purely for the CNAME check. If no type is set, set the conv-
  1178. # entional value. This may be overridden by subsequent records
  1179. # such as A or PTR.
  1180.  
  1181. $names{"$name"} = $used_other if $names{"$name"} == "";
  1182.   
  1183.  
  1184.  
  1185. # Type NS - identity of nameserver. As the zone's nameserver records were
  1186. # processed at the top of the file, these are NS records for devolved sub-
  1187. # zones. Check that the name is fully qualified (ends with dot).
  1188.  
  1189. if ($type eq "NS")
  1190.   {
  1191.   do check_fqn($rest, "NS");
  1192.  
  1193.   # If the name starts with a digit, it must be the reversed address of
  1194.   # a devolved sub-zone of a Class B network.
  1195.  
  1196.   if ($name =~ /^\d/)
  1197.     {
  1198.     local($net, $rzone);
  1199.     local($a,$b,$c) = $name =~ /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
  1200.  
  1201.     if ($a eq "")
  1202.       {
  1203.       do error("subnet name on NS record is invalid.");
  1204.       return;
  1205.       }
  1206.  
  1207.     $net = ($c << 24) | ($b << 16);
  1208.  
  1209.     for ($rzone = 0; $rzone < $rzone_count; $rzone++)
  1210.       { last if ($net == $rzone_number[$rzone]); }
  1211.  
  1212.     if ($rzone >= $rzone_count)
  1213.       {
  1214.       do error("$c.$b.$a is not a subnet of a known network.");
  1215.       }
  1216.     else
  1217.       {
  1218.       local($handle) = "REVERSE$rzone";
  1219.       print $handle "$a  $ttl  $class  NS  $rest\n";
  1220.       }
  1221.     }
  1222.  
  1223.   # Otherwise this is a devolution from the main forwards zone
  1224.  
  1225.   else { print FORWARD "$printname  $ttl  $class  NS  $rest\n"; }
  1226.   return;
  1227.   }
  1228.  
  1229.  
  1230. # Type HINFO - host information; no further checking
  1231.  
  1232. if ($type eq "HINFO")
  1233.   {
  1234.   print FORWARD "$printname  $ttl  $class  HINFO  $rest\n";
  1235.   return;
  1236.   }
  1237.  
  1238.  
  1239.  
  1240. # Type MX - mail exchanger; there must be a preference and
  1241. # a fully-qualified gateway name.
  1242.  
  1243. if ($type eq "MX")
  1244.   {
  1245.   ($pref,$gateway) = split(/\s+/, $rest, 2);
  1246.   do check_fqn($gateway, "MX");
  1247.   if ($pref !~ /^\d+$/)
  1248.     {
  1249.     do error("invalid MX preference field (not all digits).");
  1250.     }
  1251.   print FORWARD "$printname  $ttl  $class  MX  $pref  $gateway\n";
  1252.   return;
  1253.   }
  1254.  
  1255.  
  1256.  
  1257. # Type WKS - well-known services. This commonly is continued onto
  1258. # other lines, so we must handle continuations. Check the protocol
  1259. # field is either TCP or UDP, then check all the services appear
  1260. # in the $services file, if it is set (typically /etc/services).
  1261. # Check the address is in a known network, unless external.
  1262.  
  1263. if ($type eq "WKS")
  1264.   {
  1265.   ($address,$proto,$rest) = split(/\s+/, $rest, 3);
  1266.  
  1267.   # Check the address
  1268.  
  1269.   if (!$external_net)
  1270.     {
  1271.     local($a,$b,$c,$d) =
  1272.       $address =~ /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
  1273.  
  1274.     if ($a eq "")
  1275.       {
  1276.       do error("IP address on WKS record is incomplete");
  1277.       return;
  1278.       }
  1279.  
  1280.     if ($a > 255 || $b > 255 || $c > 255 || $d > 255)
  1281.       {
  1282.       do error("IP address contains component with value greater than 255.");
  1283.       return;
  1284.       }
  1285.  
  1286.     $net = ($a << 24) | ($b << 16);
  1287.     $net += ($c << 8) if $a >= 192;
  1288.  
  1289.     for ($rzone = 0; $rzone < $rzone_count; $rzone++)
  1290.       { last if ($net == $rzone_number[$rzone]); }
  1291.  
  1292.     if ($rzone >= $rzone_count)
  1293.       {
  1294.       $net = ($a >= 192)? "$a.$b.$c" : "$a.$b";
  1295.       do error("$net is not a known network.");
  1296.       }
  1297.     }
  1298.  
  1299.   # Check the protocol
  1300.  
  1301.   if ($proto ne "UDP" && $proto ne "TCP")
  1302.     {
  1303.     do error("protocol in WKS record must be \"UCP\" or \"TCP\".");
  1304.     }
  1305.  
  1306.   # Start of line prefix - the rest of the line is in $rest
  1307.  
  1308.   $pref = "$printname  $ttl  $class  WKS  $address $proto";
  1309.  
  1310.   # Allow continuation bracket at start of list only
  1311.  
  1312.   if (substr($rest, 0, 1) eq "(")
  1313.     {
  1314.     $continued = 1;
  1315.     ($list) = $rest =~ /^\(\s*(.+)$/;
  1316.     }
  1317.   else { $continued = 0; $list = $rest; }
  1318.  
  1319.   # Loop for handling continuation records
  1320.  
  1321.   for (;;)
  1322.     {
  1323.     while (substr($list, -1) eq "\n") { chop($list); }
  1324.     while (substr($list, -1) eq " ")  { chop($list); }
  1325.  
  1326.     # Loop for scanning the list of services
  1327.  
  1328.     while ($list ne "")
  1329.       {
  1330.       if (index($list, " ") >= 0)
  1331.         {
  1332.         ($servicename,$list) = split(/\s+/, $list, 2);
  1333.         }
  1334.       else
  1335.         {
  1336.         $servicename = $list;
  1337.         $list = "";
  1338.  
  1339.         # Check for closing bracket at end of line. It may or may not
  1340.         # be preceded by a space.
  1341.  
  1342.         if ($continued && substr($servicename, -1) eq ")")
  1343.           {
  1344.           chop($servicename);
  1345.           $continued = 0;
  1346.           }
  1347.         }
  1348.  
  1349.       # Check the service if required. $servicename can be empty if
  1350.       # a closing bracket is preceded by a space.
  1351.  
  1352.       if ("$services" ne "" && $servicename ne "")
  1353.         {
  1354.         if (system("$grep \'^$servicename[ \t]\' $services >/dev/null")/256)
  1355.           {
  1356.           do error("\"$servicename\" does not appear in $services");
  1357.           }
  1358.         }
  1359.       }
  1360.  
  1361.     print FORWARD "$pref  $rest\n";
  1362.     return if !$continued;
  1363.  
  1364.     # Read in the next line, which contains more services, for the
  1365.     # next time round this loop.
  1366.  
  1367.     $_ = <SOURCE>;
  1368.     $nline++;
  1369.     ($list,$dummy) = $_ =~ /^\s*([^;]+)(;.*)?$/;
  1370.     $rest = "$list";
  1371.     $pref = "  ";
  1372.     }
  1373.   }
  1374.  
  1375.  
  1376.  
  1377. # Type RP (Responsible Person) - two domain names
  1378.  
  1379. if ($type eq "RP")
  1380.   {
  1381.   if ($rest !~ /^\S+\s+\S+$/)
  1382.     {
  1383.     do error("malformed RP record - two fields required.");
  1384.     }
  1385.   print FORWARD "$printname  $ttl  $class  RP  $rest\n";
  1386.   return;
  1387.   }
  1388.  
  1389.  
  1390.  
  1391. # Else we have a bad record
  1392.  
  1393. do error("unknown record type.");
  1394. }
  1395.  
  1396.  
  1397.  
  1398.  
  1399.  
  1400. ##################################################
  1401. #           Generate the zone data               #
  1402. ##################################################
  1403.  
  1404. sub generate_zones{
  1405. local($i);
  1406.  
  1407. $lastname = "";
  1408. $nline = 0;
  1409.  
  1410. print "Generating the zone data...\n" if $chatty;
  1411.  
  1412. # Open the input file
  1413.  
  1414. open(SOURCE, "$source_file") ||
  1415.   do give_up("unable to open $source_file");
  1416.  
  1417. # Open the output files
  1418.  
  1419. open(FORWARD, ">$forward_file.new") ||
  1420.   do give_up("unable to open $forward_file.new");
  1421.  
  1422. for ($i = 0; $i < $rzone_count; $i++)
  1423.   {
  1424.   open("REVERSE$i", ">$rzone_file[$i].new") ||
  1425.     do give_up("unable to open $rzone_file[$i].new");
  1426.   }
  1427.  
  1428. # Copy the SOA record into all the output files
  1429.  
  1430. for ($nline = 1; $nline <= $soa_count; $nline++)
  1431.   {
  1432.   $_ = <SOURCE>;
  1433.   print FORWARD $_;
  1434.   do print_reverse($_);
  1435.   }
  1436.   
  1437. # Record the fact that the name "@" has been used, for a record
  1438. # of type "other". This will stop a CNAME of that name.
  1439.  
  1440. $names{"$zone_name."} = $used_other; 
  1441.  
  1442. # Copy all the NS records for these zones to all the outputs. Stop
  1443. # on reaching a non-NS record or a record with a name field. Skip
  1444. # blank lines, and handle comments as normal.
  1445.  
  1446. # We extend the syntax of NS records by allowing a list of names
  1447. # to follow the nameserver name. If this is present, it lists the
  1448. # zones to which this nameserver applies. Reverse zones are identified
  1449. # by their IP network numbers.
  1450.  
  1451. $nline--;
  1452. for (;;)
  1453.   {
  1454.   $_ = <SOURCE>;
  1455.   $nline++;
  1456.   if (/^;/) { do handle_comment(); next; }
  1457.   next if /^\s*$/;
  1458.   last if /^\S/;
  1459.    
  1460.   local($ttl,$class,$ns,$rest) = /^\s+(\d+\s+|)(IN\s+|)NS\s+(\S+)(|\s+.+)$/;
  1461.   last if $ns eq "";
  1462.   do check_fqn($ns, "NS");
  1463.  
  1464.   $rest =~ s/^\s+//;         # strip leading white space
  1465.   $rest =~ s/\s*(;.*)?$//;   # strip trailing spaces and comments & NL
  1466.   if ($rest eq "")
  1467.     {
  1468.     print FORWARD $_;
  1469.     do print_reverse($_);
  1470.     }
  1471.   else
  1472.     {
  1473.     while ($rest ne "")
  1474.       {
  1475.       ($zone,$rest) = split(/\s+/, $rest, 2);
  1476.       if ($zone eq $zone_name)
  1477.         {
  1478.         print FORWARD "  $ttl  $class  NS  $ns\n";
  1479.         }
  1480.       else
  1481.         {
  1482.         local($i);
  1483.         local($a,$b,$c,$d) =
  1484.           $zone =~ /^\s*(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\s*$/;
  1485.         if ($a eq "")
  1486.           {
  1487.           do error("wrong zone name or malformed network number on NS record");
  1488.           }
  1489.         else
  1490.           {
  1491.           $zn = ($a << 24) | ($b << 16) | ($c << 8) | $d;
  1492.           for ($i = 0; $i < $rzone_count; $i++)
  1493.             {
  1494.             if ($rzone_number[$i] == $zn)
  1495.               {
  1496.               local($handle) = "REVERSE$i";
  1497.               print $handle "  $ttl  $class  NS  $ns\n";
  1498.               last;
  1499.               }
  1500.             }
  1501.           do error("unknown network number on NS record")
  1502.             if $i >= $rzone_count;
  1503.           }
  1504.         }
  1505.       }
  1506.     }
  1507.   }
  1508.  
  1509. # OK, now we have the first general record in $_. We can now scan
  1510. # the rest of the file, processing as required. We do a check on
  1511. # the first character of the line, because it is easy in moments
  1512. # of absent-mindedness to do silly things like put in comments with
  1513. # a sharp sign character instead of a semicolon. Let through only
  1514. # those characters that can legally begin a line.
  1515.  
  1516. for (;;)
  1517.   {
  1518.   if (!/^\s*$/)
  1519.     {
  1520.     if (!/^[\s\da-zA-Z\;>\"@\*]/)
  1521.       { do error("invalid line - semicolon omitted?"); }
  1522.     elsif (substr($_, 0, 1) eq ";")
  1523.       { do handle_comment(); }
  1524.     else
  1525.       { do handle_record(); }
  1526.     }
  1527.   last if ! ($_ = <SOURCE>);
  1528.   $nline++;
  1529.   }
  1530.  
  1531. # Close all the files
  1532.  
  1533. close FORWARD;
  1534. close SOURCE;
  1535. for ($i = 0; $i < $rzone_count; $i++) { close("REVERSE$i"); }
  1536. }
  1537.  
  1538.  
  1539.  
  1540.  
  1541. ##################################################
  1542. #           Compare new/old zone lengths         #
  1543. ##################################################
  1544.  
  1545. sub check_length{
  1546. local($length_old, $length_new, $length_diff);
  1547. local($name) = $_[0];
  1548.  
  1549. if (! -e $name)
  1550.   {
  1551.   print "\n" if $lastwaserror; 
  1552.   print "  " if $chatty; 
  1553.   print "Length of $name not checked - previous version of file does not exist\n";
  1554.   $lastwaserror = 0; 
  1555.   return;
  1556.   }
  1557.  
  1558. @stat_data = stat($name);
  1559. $length_old = $stat_data[7];
  1560. @stat_data = stat("$name.new");
  1561. $length_new = $stat_data[7];
  1562. $length_diff = $length_old - $length_new;
  1563.  
  1564. if ($length_diff > ($length_old/20))
  1565.   {
  1566.   do error("$name.new is more than 5% shorter than $name.\n".
  1567.     "** Use -short to override this check.");
  1568.   $lastwaserror = 1;
  1569.   }
  1570. elsif ($chatty)
  1571.   {
  1572.   print "\n" if $lastwaserror;
  1573.   print "  Length of $name is OK\n";
  1574.   $lastwaserror = 0;
  1575.   }
  1576. }
  1577.  
  1578.  
  1579. sub compare_lengths{
  1580. local($i);
  1581. print "Comparing lengths of old and new zone files...\n" if $chatty;
  1582. $lastwaserror = 0;
  1583. do check_length("$forward_file");
  1584. for ($i = 0; $i < $rzone_count; $i++)
  1585.   {
  1586.   do check_length("$rzone_file[$i]");
  1587.   }
  1588. }
  1589.  
  1590.  
  1591.  
  1592. ##################################################
  1593. #         Rename new zones to final names        #
  1594. ##################################################
  1595.  
  1596. sub rename_zones {
  1597. local($i);
  1598. print "Renaming the new zone files to their final names...\n" if $chatty;
  1599. rename("$forward_file.new", "$forward_file");
  1600. for ($i = 0; $i < $rzone_count; $i++)
  1601.   { rename("$rzone_file[$i].new", "$rzone_file[$i]"); }
  1602. }
  1603.  
  1604.  
  1605.  
  1606. ##################################################
  1607. #           Remove temporary files               #
  1608. ##################################################
  1609.  
  1610. # This is used to remove the temporary files if processing
  1611. # fails. It is not an error for the temps not to exist.
  1612.  
  1613. sub remove_temps{
  1614. local ($i);
  1615. unlink "$forward_file.new";
  1616. for ($i = 0; $i < $rzone_count; $i++)
  1617.   { unlink "$rzone_file[$i].new"; }
  1618. }
  1619.  
  1620.  
  1621.  
  1622. ##################################################
  1623. #                Main Program                    #
  1624. ##################################################
  1625.  
  1626. # After any serious error, the script dies and does not
  1627. # return to the main code. Syntax errors etc. carry on,
  1628. # leaving $errors containing the count. Only generate_zones()
  1629. # and compare_lengths() handle errors in this way - all the 
  1630. # other routines generate hard errors.
  1631.  
  1632. $rzone_count = $errors = 0;
  1633. $soa_count = 6;
  1634.  
  1635. # Conventional values for the %names array:
  1636.  
  1637. $used_other   = -999999;
  1638. $used_reserve = -888888;
  1639. $used_dup     = -777777;
  1640.  
  1641. # Get weaving...
  1642.  
  1643. do unpick_args();
  1644. do verify();
  1645. do update_serial();
  1646. do generate_zones();
  1647. print "\n" if $errors > 0;
  1648.  
  1649. # No line number for subsequent error messages.
  1650.  
  1651. $nline = -1;
  1652.  
  1653. # If length checks successful, do renames and end happy.
  1654.  
  1655. if ($errors == 0)
  1656.   {
  1657.   do compare_lengths() if !$opt_short;
  1658.   if ($errors == 0)
  1659.     {
  1660.     do rename_zones();
  1661.     print "\nMakezones completed successfully.\n";
  1662.     exit 0;
  1663.     }
  1664.   }
  1665.  
  1666. # Something didn't work out...
  1667.  
  1668. do remove_temps();
  1669. print "\n** Makezones failed.\n";
  1670. exit 99;
  1671.  
  1672. # End of makezones
  1673.